<script lang="ts" context="module">
  import type {
    Column,
    DataSource,
    EditingRowDataState,
    TableContext,
    InlineOperateOption,
  } from "./conf";
  import TH from "./TableHead.svelte";
  import TR from "./TableRow.svelte";
  import ButtonCell from "./ButtonCell.svelte";
  import { setContext, tick } from "svelte";
  import { writable } from "svelte/store";
  import ConfirmModal from "@component/modals/ConfirmModal.svelte";
  import { DropdownButton } from "@component/dropdown";
</script>

<script lang="ts">
  import { isUndefined } from "@util/checker";

  type T = $$Generic;
  import { createEventDispatcher } from "svelte";
  const dispatch = createEventDispatcher();

  export let cols: Column[] = [];
  export let ds: DataSource<T>;
  export let checkable = false;
  export let draggable = false;
  export let editable = false;
  export let inlineOp = false;
  export let inlineOpAddGroup: Record<string, InlineOperateOption<T>> = {};

  let elConfirmModal: ConfirmModal;
  let elTable: HTMLTableElement;
  let elAll: HTMLInputElement;
  let checkedAll = false;
  let checkStates: boolean[];
  let editingRowIndex = -1;
  let draggingRowIndex = -1;
  let draggingTrIndex: number;
  let isDirty$ = writable(false);
  let isEditNewRow$ = writable(false);
  let isEditingRowDataState$ = writable<EditingRowDataState>({});
  let editingRawRow: T;

  setContext<TableContext>("table", {
    isDirty$,
    isEditingRowDataState$,
  });

  $: {
    checkStates = ds.rows.map((r) => false);
  }
  $: useInlineOpAddGroup = inlineOp && Object.keys(inlineOpAddGroup).length > 0;
  $: allTableColCnt = cols.length + (inlineOp?1:0) + (draggable?1:0);

  function test() {
    console.log("t checkedAll", checkedAll);
    console.log("t checkStates", checkStates);
  }
  function handleClickAll(e) {
    if (checkedAll) {
      checkStates = ds.rows.map((r) => false);
    } else {
      checkStates = ds.rows.map((r) => true);
    }
    calcAllCheckState();
  }
  function onCellContentChange(e) {
    const { rowIndex, cellKey, val } = e.detail;
    console.log("onCellContentChange =>", rowIndex, cellKey, val);
    const updateRow = ds.rows[rowIndex];
    updateRow[cellKey] = val;
    ds.rows = ds.rows;
    // const oldVal = updateRow[cellKey];
    // if (oldVal !== val) {
    // 	updateRow[cellKey] = val;
    // 	ds.rows = [...ds.rows];
    // }
  }
  function onRowEditingIndexChange(e) {
    console.log("---onRowEditingIndexChange---");
    changeEditingState();
    editingRowIndex = e.detail;
    editingRawRow = { ...ds.rows[editingRowIndex] };
    console.log("editingRawRow", editingRawRow);
  }
  function onRowEditingSave(e) {
    console.log("---onRowEditingSave---");
    if (editingRowIndex !== -1) {
      if ($isDirty$ && calcEditingRowDataValid()) {
        saveEditingRowToDS();
      }
    }
    editingRowIndex = -1;
  }
  function onRowEditingCancel(e) {
    console.log("---onRowEditingCancel---");
    if (editingRowIndex !== -1) {
      console.log("editingRawRow", editingRawRow);
      ds.rows[editingRowIndex] = editingRawRow;
      if ($isEditNewRow$) {
        abandonNewRowToDS();
      } else {
        abandonExistRowToDS();
      }
    }
    editingRowIndex = -1;
  }
  function onRowSelect(e) {
    const { index, checked } = e.detail;
    if (!checkable) {
      checkStates = checkStates.map((_, idx) =>
        index === idx ? checked : false
      );
    }
    dispatch("select-row", e.detail);
  }
  function onRowRemove(e) {
    console.log("----onRowRemove-----", e.detail);
    elConfirmModal = new ConfirmModal({
      props: {
        desc: "确定要删除当前行吗?",
      },
      target: document.body,
    });
    elConfirmModal.$on("modal-confirm", () => {
      console.warn("确定要删除当前行...");
      elConfirmModal.$destroy();
      const index = e.detail;
      let deleted = ds.rows.splice(index, 1);
      ds.rows = ds.rows;
      dispatch("table-row-removed", {
        row: deleted[0],
      });
      dispatch("table-row-changed", {
        rows: ds.rows,
      });
    });
    elConfirmModal.$on("modal-cancel", () => {
      elConfirmModal.$destroy();
    });
    elConfirmModal.show();
  }
  function handleCreateNew(option?: InlineOperateOption<T>) {
    let newRowFunc = ds.newDefaultRow;
    if (!isUndefined(option)) {
      newRowFunc = option.handler;
    }
    ds.rows = [...ds.rows, newRowFunc()];
    editingRowIndex = ds.rows.length - 1;
    console.log("handleCreateNew...");
    isEditNewRow$.set(true);
  }
  function onRowCreateNew(e) {
    console.log("----onRowCreateNew-----", e.detail);
    const newIndex = e.detail + 1;
    ds.rows.splice(newIndex, 0, ds.newDefaultRow());
    // ds.rows = [...ds.rows];
    ds.rows = ds.rows;
    editingRowIndex = newIndex;
    isEditingRowDataState$.set({});
    dispatch("table-row-changed", {
      rows: ds.rows,
    });
  }
  function onRowCheckChange(e) {
    const { index, checked } = e.detail;
    checkStates[index] = checked;
    calcAllCheckState();
  }
  function calcAllCheckState() {
    checkedAll = checkStates.every((r) => r);
    elAll.indeterminate = checkStates.some((r) => r) && !checkedAll;
    let checkedIndexes: number[] = [];
    checkStates.forEach((v, idx) => {
      if (v) {
        checkedIndexes.push(idx);
      }
    });
    dispatch("check-rows", {
      checkedIndexes,
    });
  }
  function calcEditingRowDataValid() {
    let isValid = true;
    console.log("--calcEditingRowDataValid--", $isEditingRowDataState$);
    for (let key of Object.keys($isEditingRowDataState$)) {
      isValid = isValid && $isEditingRowDataState$[key].valid;
      if (!isValid) {
        // console.warn(`key:${key} is invalid with oldVal:${$isEditingRowDataState$[key].oldValue} and newVal:${$isEditingRowDataState$[key].newValue}`)
        break;
      }
    }
    return isValid;
  }
  function handleLostTableFocus(e) {
    let pel = e.target?.parentElement;
    // console.log('handleLostTableFocus=>',e.target, e.target.parentElement.closest('table'))
    if (!pel || pel.closest("table") === elTable) {
    } else {
      inlineOp && changeEditingState();
    }
  }

  // 包括editingRowIndex切换或者lostTableFocus
  function changeEditingState() {
    console.log("$isDirty$=>", $isDirty$);
    console.log("$isEditNewRow$=>", $isEditNewRow$);
    if (editingRowIndex !== -1) {
      if ($isEditNewRow$) {
        if ($isDirty$) {
          if (calcEditingRowDataValid()) {
            console.log("new row dirty...");
            // elConfirmModal.show();
            saveEditingRowToDS();
          }
          return;
        }
        abandonNewRowToDS();
      } else {
        // console.log("$isEditRowDataValid$=>", $isEditRowDataValid$);
        if ($isDirty$) {
          if (calcEditingRowDataValid()) {
            saveEditingRowToDS();
          }
        } else {
          abandonExistRowToDS();
        }
      }
    }
  }

  function abandonNewRowToDS() {
    console.log("abandon new row ...");
    ds.rows.pop();
    editingRowIndex = -1;
    isDirty$.set(false);
    isEditNewRow$.set(false);
    dispatch("table-row-changed", {
      rows: ds.rows,
    });
  }
  function abandonExistRowToDS() {
    console.log("abandon exist row ...");
    editingRowIndex = -1;
    editingRowIndex = -1;
  }
  function saveEditingRowToDS() {
    console.log("save editing row ...");

    isDirty$.set(false);
    if ($isEditNewRow$) {
      dispatch("table-row-created", {
        row: ds.rows[editingRowIndex],
      });
      isEditNewRow$.set(false);
    } else {
      dispatch("table-row-updated", {
        row: ds.rows[editingRowIndex],
      });
    }
    editingRowIndex = -1;
    dispatch("table-row-changed", {
      rows: ds.rows,
    });
  }
  function loadMore() {
    dispatch("table-load-more", {
      page_no: ds.pageNo + 1,
      page_size: ds.pageSize,
    });
  }

  let draggingEl: HTMLDivElement;

  let isDraggingStarted = false;
  let list: HTMLDivElement;
  let placeholder: HTMLDivElement;

  // holds current position of mouse relative to dragging element
  let x = 0;
  let y = 0;
  function cloneTable() {
    const rect = elTable.getBoundingClientRect();
    const width = parseInt(window.getComputedStyle(elTable).width);
    list = document.createElement("div");
    list.classList.add("dt-clone-list");
    list.style.position = "absolute";
    list.style.left = `${rect.left}px`;
    list.style.top = `${rect.top}px`;
    elTable.parentNode.insertBefore(list, elTable);
    // Hide the original table.
    elTable.style.visibility = "hidden";
    for (const row of elTable.querySelectorAll("tr")) {
      // Create a new table from given row.
      const item = document.createElement("div");
      item.classList.add("dt-draggable");
      const newTable = document.createElement("table");
      newTable.setAttribute("class", "clone-table");
      newTable.style.width = `${width}px`;
      const newRow = document.createElement("tr");
      for (const cell of row.children) {
        const newCell = cell.cloneNode(true) as HTMLTableCellElement;
        newCell.style.width = `${parseInt(
          window.getComputedStyle(cell).width
        )}px`;
        newRow.appendChild(newCell);
      }
      newTable.appendChild(newRow);
      item.appendChild(newTable);
      list.appendChild(item);
    }
  }
  function isAbove(nodeA: Element, nodeB: Element): boolean {
    // Get the bounding rectangle of nodes.
    const rectA = nodeA.getBoundingClientRect();
    const rectB = nodeB.getBoundingClientRect();
    return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2;
  }
  function mouseDownHandler(e: MouseEvent): void {
    // Get table row containing target.
    const target = e.target as Element;
    let originalRow = target.parentNode;
    while (originalRow.nodeName !== "TR") {
      originalRow = originalRow.parentNode;
    }
    const trs = Array.from(elTable.querySelectorAll("tr"));
    draggingTrIndex = trs.indexOf(originalRow as HTMLTableRowElement);
    draggingRowIndex = draggingTrIndex - 1; // 数据行索引需要减去header
    // Determine mouse position.
    x = e.clientX;
    y = e.clientY;
    // Attach the listeners to document.
    document.addEventListener("mousemove", mouseMoveHandler);
    document.addEventListener("mouseup", mouseUpHandler);
  }
  function mouseMoveHandler(e: MouseEvent): void {
    if (!isDraggingStarted) {
      isDraggingStarted = true;
      cloneTable();
      draggingEl = list.children.item(draggingTrIndex) as HTMLDivElement; // 计算header
      console.log("draggingEl=>", draggingEl);
      draggingEl.classList.add("dt-dragging");
      placeholder = document.createElement("div");
      placeholder.classList.add("dt-placeholder");
      placeholder.style.height = `${draggingEl.offsetHeight}px`;
      draggingEl.parentNode.insertBefore(placeholder, draggingEl.nextSibling);
    }
    // Set position for dragging element.
    draggingEl.style.position = "absolute";
    draggingEl.style.top = `${draggingEl.offsetTop + e.clientY - y}px`;
    draggingEl.style.left = `${draggingEl.offsetLeft + e.clientX - x}px`;
    // Reassign position of mouse.
    x = e.clientX;
    y = e.clientY;
    // The current order is prevEle, draggingEl, placeholder, nextEle.
    const prevEle = draggingEl.previousElementSibling;
    const nextEle = placeholder.nextElementSibling;
    // If the dragging element is above the previous element
    // and the user moves the dragging element to the top,
    // don't allow to drop above the header
    // (which doesn't have `previousElementSibling`).
    if (
      prevEle &&
      prevEle.previousElementSibling &&
      isAbove(draggingEl, prevEle)
    ) {
      // current order -> new order
      // prevEle       -> placeholder
      // draggingEl    -> draggingEl
      // placeholder   -> prevEle
      swapElements(placeholder, draggingEl);
      swapElements(placeholder, prevEle);
      return;
    }
    // If the dragging element is below the next element
    // and the ser moves the dragging element to the bottom ...
    if (nextEle && isAbove(nextEle, draggingEl)) {
      // current order -> new order
      // draggingEl    -> nextEle
      // placeholder   -> placeholder
      // nextEle       -> draggingEl
      swapElements(nextEle, placeholder);
      swapElements(nextEle, draggingEl);
    }
  }
  function mouseUpHandler(): void {
    if (!placeholder) return; // not dragging
    // Remove placeholder.
    if (placeholder) placeholder.parentNode.removeChild(placeholder);
    draggingEl.classList.remove("dt-dragging");
    draggingEl.style.removeProperty("top");
    draggingEl.style.removeProperty("left");
    draggingEl.style.removeProperty("position");
    const endTrIndex = Array.from(list.children).indexOf(draggingEl); //排除header
    isDraggingStarted = false;
    // Remove list element.
    list.parentNode.removeChild(list);

    // Move dragged row to endRowIndex.
    // const rows = Array.from(elTable.querySelectorAll('tr'));
    // draggingTrIndex > endTrIndex
    //   ? rows[endTrIndex].parentNode.insertBefore(
    //       rows[draggingTrIndex],
    //       rows[endTrIndex]
    //     )
    //   : rows[endTrIndex].parentNode.insertBefore(
    //       rows[draggingTrIndex],
    //       rows[endTrIndex].nextSibling
    //     );

    // 用数据驱动
    const endRowIndex = endTrIndex - 1;
    const draggingRow = ds.rows.splice(draggingRowIndex, 1)[0];
    ds.rows.splice(endRowIndex, 0, draggingRow);
    ds.rows = [...ds.rows];

    // Bring back the table.
    elTable.style.removeProperty("visibility");
    // Remove handlers of mousemove and mouseup.
    document.removeEventListener("mousemove", mouseMoveHandler);
    document.removeEventListener("mouseup", mouseUpHandler);
    // console.log('mouse up handler =>',draggingTrIndex, draggingRowIndex, endTrIndex, ds.rows);
    draggingRowIndex = -1;
  }

  function swapElements(elementA: Element, elementB: Element) {
    const siblingA =
      elementA.nextSibling === elementB ? elementA : elementA.nextSibling;
    // Move elementA to before the elementB.
    elementB.parentNode.insertBefore(elementA, elementB);
    // Move elementB to before the sibling of elementA.
    elementA.parentNode.insertBefore(elementB, siblingA);
  }

  function makeDraggable(el: HTMLTableCellElement) {
    el.addEventListener("mousedown", mouseDownHandler);
    return {
      destroy() {
        // the node has been removed from the DOM
        el.removeEventListener("mousedown", mouseDownHandler);
      },
    };
  }
</script>

<!-- <button on:click={test} >测试</button> -->
<svelte:body on:click={handleLostTableFocus} />
<div class="dt-table-container">
  <table bind:this={elTable} class="w-full overflow-x-auto">
    <thead class="dt-header-bg">
      <tr>
        {#if draggable}
          <th style="width:1px; no-wrap" />
        {/if}
        {#if checkable}
          <th class="dt-th-checkbox-all w-8 px-2" class:hidden={!checkable}>
            <input
              bind:this={elAll}
              type="checkbox"
              name="All"
              class="col-checkbox"
              bind:checked={checkedAll}
              on:click={handleClickAll}
            />
          </th>
        {/if}
        {#each cols as col}
          <TH {col} />
        {/each}
        {#if inlineOp}
          <th style="width:1px; no-wrap">
            {#if useInlineOpAddGroup}
              <DropdownButton
                direction="rt"
                --dd-border-width="0px"
                --dd-content-width={80}
              >
                <div class="w-full dropdown-cell" slot="button-content">
                  添加
                </div>
                <ul>
                  {#each Object.entries(inlineOpAddGroup) as [key, option]}
                    <!-- svelte-ignore a11y-click-events-have-key-events -->
                    <li
                      class="ddb-item"
                      on:click={() => handleCreateNew(option)}
                    >
                      {option.text}
                    </li>
                  {/each}
                </ul>
              </DropdownButton>
            {:else}
              <ButtonCell icon="plus" action={handleCreateNew} />
            {/if}
          </th>
        {/if}
      </tr>
    </thead>
    <tbody>
      {#if ds && ds.rows.length > 0}
        {#each ds.rows as dr, idx}
          <TR
            {cols}
            rowData={dr}
            {checkable}
            {editable}
            {draggable}
            {makeDraggable}
            {inlineOp}
            {editingRowIndex}
            checked={checkStates[idx]}
            rowIndex={idx}
            on:select-row={onRowSelect}
            on:change-row-checked={onRowCheckChange}
            on:change-row-editingindex={onRowEditingIndexChange}
            on:save-row-editing={onRowEditingSave}
            on:cancel-row-editing={onRowEditingCancel}
            on:remove-row={onRowRemove}
            on:create-new-row={onRowCreateNew}
            on:change-cell-content={onCellContentChange}
          />
        {/each}
      {/if}
    </tbody>
    <tfoot>
      <tr>
        <td class="dt-foot" colspan={allTableColCnt} align="center">
          {#if ds.rows.length < ds.total}
          <button on:click={loadMore}>
            加载更多...
          </button>
          {:else if ds.rows.length > 0}
            <span class="text-sm italic">总共{ds.total}条数据以加载完毕</span>
          {:else}
          <span class="text-sm italic">no data</span>
          {/if}
        </td>
      </tr>
    </tfoot>
    <!---->
  </table>
</div>

<style lang="postcss">
  :root {
    --dt-height: 100%;
    --dt-col-width: 128;
    --dt-row-height: 48;
    --dt-head-align: left;
    --dt-padding-left: 0.5rem;
    --dt-padding-right: 0.5rem;
    --dt-padding-top: 0.75rem;
    --dt-padding-bottom: 0.75rem;
  }
  .dt-table-container {
    @apply w-full p-2 mx-auto overflow-y-scroll;
    height: calc(var(--dt-height) * 1px);
  }
  .dt-th-checkbox-all {
    @apply w-8 px-2 pt-2;
    height: calc(var(--dt-row-height) * 1px);
    vertical-align: middle;
  }
  .dt-header-bg {
    background-color: #374151;
  }
  .dt-foot {
    @apply py-2 text-xl;
  }

  :global(.dt-placeholder) {
    --border-width: 2px;
    --fudge: 3px;
    border: var(--border-width) dashed #21262d;
    height: calc(var(--dt-row-height) - 2 * var(--border-width) - var(--fudge));
  }
  :global(.dt-dragging) {
    background: #161b22;
  }
  :global(.dt-clone-list td) {
    /* border-top-width: 0; */
    height: calc(var(--dt-row-height) * 1px);
  }
  :global(.dt-dragging td) {
    @apply border-b;
    border-color: #30363d;
  }
  :global(.dt-draggable) {
    cursor: move;
    user-select: none;
  }
</style>
